﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Windows;
using System.Windows.Controls;

namespace BrasilFramework.Comum.Silverlight.Utilitario
{
    public static class Clone
    {

        private static List<FieldInfo> _attachedProperties;

        public static T Clonar<T>(this T fonte, IEnumerable<Type> tiposConhecidos = null)
        {
            DataContractSerializer contrato = new DataContractSerializer(typeof(T), tiposConhecidos);

            using (MemoryStream memoria = new MemoryStream())
            {
                contrato.WriteObject(memoria, fonte);
                memoria.Position = 0;
                return (T)contrato.ReadObject(memoria);
            }

        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TT"></typeparam>
        /// <param name="source"></param>
        /// <param name="cloneAttachedProperties"></param>
        /// <returns></returns>
        /// <remarks>Jim McCurdy's DeepClone</remarks>
        public static TT DeepClone<TT>(this TT source, bool? cloneAttachedProperties = null) where TT : class
        {
            if (cloneAttachedProperties == null)
                cloneAttachedProperties = (source is DependencyObject);

            // The first time this method is called, compute a list of all  
            // attached properties that exist in this XAP's assemblies  
            if (cloneAttachedProperties == true && _attachedProperties == null)
            {

                _attachedProperties = new List<FieldInfo>();

                IEnumerable<Assembly> assemblies = GetLoadedAssemblies();

                foreach (Assembly assembly in assemblies)
                    GetAttachedProperties(_attachedProperties, assembly);
            }

            TT clone = CloneRecursive(source);

            if (clone is FrameworkElement)
            {
                FrameworkElement cloneElement = (clone as FrameworkElement);
                cloneElement.Arrange(new Rect(0, 0, cloneElement.ActualWidth, cloneElement.ActualHeight));
            }

            return clone;
        }

        private static TT CloneRecursive<TT>(TT source) where TT : class
        {
            if (source == null || source.GetType().IsValueType)
                return source;

            // Common types that do not have parameterless constructors  
            if (source is string || source is Type || source is Uri || source is DependencyProperty)
                return source;

            TT clone = CloneCreate(source);

            if (clone == null)
                return source;

            if (source is IList)
                CloneList(source as IList, clone as IList);

            CloneProperties(source, clone);

            return clone;
        }

        private static TT CloneCreate<TT>(TT source)
        {

            try
            {

#if DEBUG_TRACE  

                string.Format("Clone create object Type={0}", SimpleType(source.GetType())).Trace();  

#endif

                Array sourceArray = (source as Array);

                if (sourceArray == null)

                    return (TT)Activator.CreateInstance(source.GetType());

                if (sourceArray.Rank == 1)

                    return (TT)(object)Array.CreateInstance(source.GetType().GetElementType(),

                        sourceArray.GetLength(0));

                if (sourceArray.Rank == 2)

                    return (TT)(object)Array.CreateInstance(source.GetType().GetElementType(),

                        sourceArray.GetLength(0), sourceArray.GetLength(1));

            }

            catch (Exception ex)
            {

                if (ex.Message.Contains("No parameterless constructor"))

                    return default(TT);

                string.Format("Can't create object Type={0}", SimpleType(source.GetType())).Trace();

                ex.DebugOutput();

            }



            return default(TT);

        }

        private static void CloneProperties(object source, object clone)
        {

            // The binding flags indicate what properties we will clone  

            // Unfortunately, we cannot clone "internal" or "protected" properties  

            const BindingFlags flags = BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public;



            if (source is DependencyObject)
            {

                DependencyObject sourcedp = source as DependencyObject;

                DependencyObject clonedp = clone as DependencyObject;



                // Clone attached properties  

                if (_attachedProperties != null && _attachedProperties.Count > 0)

                    foreach (FieldInfo field in _attachedProperties)

                        CloneDependencyProperty(sourcedp, clonedp, field, true);



                // Clone dependency properties  

                FieldInfo[] fields = source.GetType().GetFields(flags | BindingFlags.Static);

                foreach (FieldInfo field in fields)

                    CloneDependencyProperty(sourcedp, clonedp, field, false);

            }



            // Clone CLR properties  

            PropertyInfo[] properties = source.GetType().GetProperties(flags);

            foreach (PropertyInfo property in properties)

                CloneProperty(source, clone, property);

        }

        private static void CloneDependencyProperty(DependencyObject sourceObject,
            DependencyObject cloneObject, FieldInfo field, bool isAttached)
        {

            try
            {

                // Blacklisted properties that can't (or shouldn't) be set  

                if (field.Name == "NameProperty" && sourceObject is FrameworkElement) return;



                DependencyProperty dp = field.GetValue(sourceObject) as DependencyProperty;

                if (dp == null) // Event DependencyProperties will be null  

                    return;



                object sourceValue = null;

                try
                {
                    sourceValue = sourceObject.GetValue(dp);
                }
                catch (Exception)
                {
                }

                if (sourceValue == null)

                    return;

                // Don't set attached properties if we don't have to  
                if (isAttached)
                {

                    Type sourceType = sourceValue.GetType();

                    if (sourceType.IsValueType && sourceValue.Equals(Activator.CreateInstance(sourceType)))

                        return;

                }

                // Blacklisted properties that can't (or don't need to be) cloned  
                bool doClone = true;
                if (field.Name == "DataContextProperty") doClone = false;

                object cloneValue = (doClone ? CloneRecursive(sourceValue) : sourceValue);

                cloneObject.SetValue(dp, cloneValue);
            }
            catch (Exception ex)
            {

                if (ex.Message.Contains("read-only")) return;

                if (ex.Message.Contains("read only")) return;

                if (ex.Message.Contains("does not fall within the expected range")) return;

                string.Format("Can't clone dependency property Name={0}, for source Type={1}",
                    field.Name, SimpleType(sourceObject.GetType())).Trace();

                ex.DebugOutput();
            }

        }

        private static void CloneProperty(object source, object clone, PropertyInfo property)
        {

            try
            {

                if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length != 0) return;

                // Blacklisted properties that can't (or shouldn't) be set  
                if (property.Name == "Name" && source is FrameworkElement) return;

                if (property.Name == "InputScope" && source is TextBox) return; // Can't get  

                if (property.Name == "Watermark" && source is TextBox) return; // Can't get  

                if (property.Name == "Source" && source is ResourceDictionary) return; // Can't set  

                if (property.Name == "TargetType" && source is ControlTemplate) return; // Can't set  

                bool publicSetter = (source.GetType().GetMethod("set_" + property.Name) != null);

                bool isList = (property.PropertyType.GetInterface("IList", true) != null);

                if (!publicSetter && !isList) return;

                object sourceValue = property.GetValue(source, null);

                if (sourceValue == null) return;

                if (!publicSetter)
                {
                    IList cloneList = property.GetValue(clone, null) as IList;

                    if (cloneList != null)
                        CloneList(sourceValue as IList, cloneList);

                    return;

                }

                // Blacklisted properties that can't (or don't need to be) cloned  
                bool doClone = true;

                if (source is FrameworkElement && property.Name == "DataContext") doClone = false;

                object cloneValue = (doClone ? CloneRecursive(sourceValue) : sourceValue);

                property.SetValue(clone, cloneValue, null); // possible MethodAccessException  

            }
            catch (Exception ex)
            {
                string.Format("Can't clone property Type={0}, Name={1}, for source Type={2}",
                    SimpleType(property.PropertyType), property.Name, SimpleType(source.GetType())).Trace();

                ex.DebugOutput();

            }

        }

        private static void CloneList(IList sourceList, IList cloneList)
        {

            try
            {

                IEnumerator sourceEnumerator = sourceList.GetEnumerator();

                Array sourceArray = sourceList as Array;

                Array cloneArray = cloneList as Array;

                int dim0 = (sourceArray != null && sourceArray.Rank > 0 ? sourceArray.GetLowerBound(0) : 0);

                int dim1 = (sourceArray != null && sourceArray.Rank > 1 ? sourceArray.GetLowerBound(1) : 0);

                while (sourceEnumerator.MoveNext())
                {

                    object sourceValue = sourceEnumerator.Current;

#if DEBUG_TRACE  
                    string.Format("Clone IList item {0}", sourceValue).Trace();  
#endif

                    object cloneValue = CloneRecursive(sourceValue);

                    if (sourceArray == null)

                        cloneList.Add(cloneValue);

                    else

                        switch (sourceArray.Rank)
                        {
                            case 1:
                                if (cloneArray != null) cloneArray.SetValue(cloneValue, dim0++);
                                break;
                            case 2:
                                if (cloneArray != null) cloneArray.SetValue(cloneValue, dim0, dim1);
                                if (++dim1 > sourceArray.GetUpperBound(1))
                                {

                                    dim1 = sourceArray.GetLowerBound(1);

                                    if (++dim0 > sourceArray.GetUpperBound(0))

                                        dim0 = sourceArray.GetLowerBound(0);

                                }
                                break;
                        }

                }

            }

            catch (Exception ex)
            {

                string.Format("Can't clone IList item Type={0}", SimpleType(sourceList.GetType())).Trace();

                ex.DebugOutput();

            }

        }

        private static string SimpleType(Type type)
        {

            string typeName = type.ToString();

            int index = typeName.LastIndexOf('[');

            if (index < 0)

                return typeName.Substring(typeName.LastIndexOf('.') + 1);



            string collectionName = typeName.Substring(index);

            collectionName = collectionName.Substring(collectionName.LastIndexOf('.') + 1);

            typeName = typeName.Substring(0, index);

            typeName = typeName.Substring(typeName.LastIndexOf('.') + 1);

            return typeName + '[' + collectionName;

        }

        private static IEnumerable<Assembly> GetLoadedAssemblies()
        {

            List<Assembly> assemblies = new List<Assembly>();

            foreach (Assembly assembly in
                Deployment.Current.Parts.Select(part => Application.GetResourceStream(new Uri(part.Source, UriKind.Relative))).Where(sri => sri != null).Select(sri => new AssemblyPart().Load(sri.Stream)).Where(assembly => assembly != null && !assemblies.Contains(assembly)))
            {
                assemblies.Add(assembly);
            }

            // Additional assemblies that are not found when examining of Deployment.Current.Parts above  
            Type[] types =  

            {  

                typeof(Application), // System.Windows.dll,  

                #if INCLUDE_ASSEMBLIES_WITHOUT_ATTACHED_PROPERTIES  

                typeof(System.Action), // mscorlib.dll,  

                typeof(System.Uri), // System.dll,  

                typeof(System.Lazy<int>), // System.Core.dll,  

                typeof(System.Net.Cookie), // System.Net.dll,  

                typeof(System.Runtime.Serialization.StreamingContext), // System.Runtime.Serialization.dll,  

                typeof(System.ServiceModel.XmlSerializerFormatAttribute), // System.ServiceModel.dll,  

                typeof(System.Windows.Browser.BrowserInformation), // System.Windows.Browser.dll,  

                typeof(System.Xml.ConformanceLevel), // System.Xml.dll,  

                #endif  

            };

            foreach (Assembly assembly in
                types.Select(type => type.Assembly).Where(assembly => assembly != null && !assemblies.Contains(assembly)))
            {
                assemblies.Add(assembly);
            }

            return assemblies;
        }

        private static void GetAttachedProperties(ICollection<FieldInfo> attachedProperties, Assembly assembly)
        {

            const BindingFlags flags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static;

            foreach (Type type in assembly.GetTypes())
            {

                FieldInfo[] fields = type.GetFields(flags);

                MethodInfo[] methods = null;

                foreach (FieldInfo field in fields)
                {

                    if (!field.FieldType.Is(typeof(DependencyProperty))) continue;

                    if (!field.Name.EndsWith("Property")) continue;

                    string fieldName = field.Name.Replace("Property", "");

                    string getName = "Get" + fieldName;

                    string setName = "Set" + fieldName;

                    bool foundGet = false;

                    bool foundSet = false;

                    if (methods == null)
                        methods = type.GetMethods(flags);

                    foreach (MethodInfo method in methods)
                    {
                        if (method.Name == getName && method.GetParameters().Length == 1 &&
                            method.GetParameters()[0].ParameterType.Is(typeof(DependencyObject)))
                            foundGet = true;
                        else
                            if (method.Name == setName && method.GetParameters().Length == 2 &&
                                method.GetParameters()[0].ParameterType.Is(typeof(DependencyObject)))
                                foundSet = true;

                        if (foundGet && foundSet) break;
                    }

                    if (!(foundGet && foundSet)) continue;

                    try
                    {
                        DependencyProperty dp = (DependencyProperty)field.GetValue(null);
                    }

                    catch (Exception)
                    {
                        continue;
                    }

                    // Found an attached Dependency Property  
                    attachedProperties.Add(field);
                }

            }

            return;
        }

    }

    internal static class Extensions
    {

        internal static bool Is(this Type type, Type baseType)
        {
            return (type.Equals(baseType) || type.IsSubclassOf(baseType));
        }

        internal static void DebugOutput(this Exception ex)
        {

#if DEBUG
            Debug.WriteLine(ex.GetType() + ": " + ex.Message + Environment.NewLine);
#endif

        }



        // Extension for string  

        internal static void Trace(this string message)
        {
            Debug.WriteLine(message);

        }

    }
}
